﻿using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;

using Microsoft.CodeAnalysis;

namespace CodeGenerators
{
	/// <summary></summary>
	[Generator]
	public partial class BuildersGenerator : IIncrementalGenerator
	{
		/// <summary></summary>
		public void Initialize(IncrementalGeneratorInitializationContext context)
		{
			var buildsAnyNodes = context.SyntaxProvider
				.ForAttributeWithMetadataName(
					"LinqToDB.Internal.Linq.Builder.BuildsAnyAttribute",
					(_, _) => true,
					TransformBuildsAny
				)
				.Collect();

			var buildsExpressionNodes = context.SyntaxProvider
				.ForAttributeWithMetadataName(
					"LinqToDB.Internal.Linq.Builder.BuildsExpressionAttribute",
					(_, _) => true,
					TransformBuildsExpression
				)
				.SelectMany((x, ct) => x)
				.Collect();

			var buildsMethodCallNodes = context.SyntaxProvider
				.ForAttributeWithMetadataName(
					"LinqToDB.Internal.Linq.Builder.BuildsMethodCallAttribute",
					(_, _) => true,
					TransformBuildsMethodCall
				)
				.SelectMany((x, ct) => x)
				.Collect();

			context
				.RegisterImplementationSourceOutput(
					buildsAnyNodes
						.Combine(buildsExpressionNodes)
						.Combine(buildsMethodCallNodes),
					(spc, x) => GenerateCode(
						spc,
						x.Left.Left,
						x.Left.Right,
						x.Right
					)
				);
		}

		static BuilderNode TransformBuildsAny(
			GeneratorAttributeSyntaxContext context,
			CancellationToken token)
		{
			token.ThrowIfCancellationRequested();

			var symbol = (INamedTypeSymbol)context.TargetSymbol;
			var className = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

			var parameters = GetMethodParameters(symbol, "CanBuild");

			return new(className, "", BuilderKind.Any, "CanBuild", parameters);
		}

		static EquatableReadOnlyList<BuilderNode> TransformBuildsExpression(
			GeneratorAttributeSyntaxContext context,
			CancellationToken token)
		{
			token.ThrowIfCancellationRequested();

			var symbol = (INamedTypeSymbol)context.TargetSymbol;
			var className = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
			var nodes = new List<BuilderNode>();

			foreach (var attribute in context.Attributes)
			{
				token.ThrowIfCancellationRequested();

				var check = attribute.NamedArguments
					.FirstOrDefault(a => a.Key == "CanBuildName")
					.Value.Value?.ToString()
					?? "CanBuild";

				var parameters = GetMethodParameters(symbol, check);

				foreach (var argument in attribute.ConstructorArguments)
				{
					nodes.Add(new(
						className,
						((ExpressionType)(int)argument.Value!).ToString(),
						BuilderKind.Expr,
						check,
						parameters
					));
				}
			}

			return nodes.ToEquatableReadOnlyList();
		}

		private static CallParams GetMethodParameters(INamedTypeSymbol type, string methodName)
		{
			var method = type.GetMembers(methodName).OfType<IMethodSymbol>().SingleOrDefault()
					?? throw new InvalidOperationException($"Cannot find method {methodName} in {type.Name}");

			var parameters = CallParams.None;
			foreach (var parameter in method.Parameters)
			{
				parameters |= parameter.Type.Name switch
				{
					"MethodCallExpression" or "Expression" => CallParams.Call,
					"BuildInfo" => CallParams.Info,
					"ExpressionBuilder" => CallParams.Builder,
					_ => throw new InvalidOperationException($"Unknown method {type.Name}.{methodName} parameter type: {parameter.Type.Name}")
				};
			}

			return parameters;
		}

		static EquatableReadOnlyList<BuilderNode> TransformBuildsMethodCall(
			GeneratorAttributeSyntaxContext context,
			CancellationToken token)
		{
			token.ThrowIfCancellationRequested();
	
			var symbol = (INamedTypeSymbol)context.TargetSymbol;
			var className = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
			var nodes = new List<BuilderNode>();

			foreach (var attribute in context.Attributes)
			{
				var check = attribute.NamedArguments
					.FirstOrDefault(a => a.Key == "CanBuildName")
					.Value.Value?.ToString()
					?? "CanBuildMethod";

				token.ThrowIfCancellationRequested();

				var parameters = GetMethodParameters(symbol, check);

				foreach (var argument in attribute.ConstructorArguments)
				{
					nodes.Add(new(
						className,
						argument.Value!.ToString(),
						BuilderKind.Call,
						check,
						parameters
					));
				}
			}

			return nodes.ToEquatableReadOnlyList();
		}

		void GenerateCode(
			SourceProductionContext context,
			ImmutableArray<BuilderNode> anyNodes,
			ImmutableArray<BuilderNode> expressionNodes,
			ImmutableArray<BuilderNode> methodCallNodes)
		{
			var source =
				$$"""
				// <auto-generated />
				#nullable enable

				using LinqToDB.Internal.Expressions;
				using System.Diagnostics.CodeAnalysis;
				using System.Linq.Expressions;

				namespace LinqToDB.Internal.Linq.Builder;
			
				partial class ExpressionBuilder
				{
					private static class Builder<T> where T: ISequenceBuilder, new()
					{
						public static readonly T Instance = new();
					}

					private static partial ISequenceBuilder? FindBuilderImpl(BuildInfo info, ExpressionBuilder builder)
					{
						var expr = info.Expression = info.Expression.Unwrap();
						switch (expr.NodeType)
						{
				{{RenderExpressionNodes(expressionNodes)}}
							case ExpressionType.Call:
							{
								var call = (MethodCallExpression)expr;
								switch (call.Method.Name)
								{
				{{RenderMethodCallNodes(methodCallNodes)}}
								}

				{{RenderExpressionCallNodes(expressionNodes)}}
								break;
							}
						}

				{{RenderAnyNodes(anyNodes)}}
						return null;
					}
				}
				""";

			context.AddSource("ExpressionBuilder.g.cs", source);
		}

		static string RenderExpressionNodes(ImmutableArray<BuilderNode> expressionNodes) =>
			string.Join(
				"",
				expressionNodes
					.Where(n => n.Key != "Call")
					.GroupBy(n => n.Key)
					.Select(g =>
						$$"""
									case ExpressionType.{{g.Key}}:

						""" +
						string.Join(
							"",
							g.Select(RenderExpressionNode)
						) +
						"""
										break;


						"""
					)
			);

		static string RenderExpressionNode(BuilderNode n) =>
			n.Params switch
			{
				CallParams.Call | CallParams.Info | CallParams.Builder =>
			$$"""
							if ({{n.Builder}}.{{n.Check}}(expr, info, builder))
								return Builder<{{n.Builder}}>.Instance;


			""",
				CallParams.Call | CallParams.Builder =>
			$$"""
							if ({{n.Builder}}.{{n.Check}}(expr, builder))
								return Builder<{{n.Builder}}>.Instance;


			""",
				_ => throw new InvalidOperationException($"{nameof(RenderExpressionNode)} doesn't implement call with {n.Params} parameters")
			};

		static string RenderMethodCallNodes(ImmutableArray<BuilderNode> expressionNodes) =>
			string.Join(
				"",
				expressionNodes
					.GroupBy(n => n.Key)
					.OrderBy(n => n.Key)
					.Select(g =>
						$$"""
											case "{{g.Key}}":

						""" +
						string.Join(
							"",
							g.Select(RenderCallNode)
						) +
						"""
												break;


						"""
					)
			);

		static string RenderCallNode(BuilderNode n) =>
			n.Params switch
			{
				CallParams.Call | CallParams.Info | CallParams.Builder =>
			$$"""
									if ({{n.Builder}}.{{n.Check}}(call, info, builder))
										return Builder<{{n.Builder}}>.Instance;


			""",
                CallParams.Call | CallParams.Builder =>
            $$"""
									if ({{n.Builder}}.{{n.Check}}(call, builder))
										return Builder<{{n.Builder}}>.Instance;


			""",
                CallParams.Call =>
			$$"""
									if ({{n.Builder}}.{{n.Check}}(call))
										return Builder<{{n.Builder}}>.Instance;


			""",
				CallParams.None =>
			$$"""
									if ({{n.Builder}}.{{n.Check}}())
										return Builder<{{n.Builder}}>.Instance;


			""",
				_ => throw new InvalidOperationException($"{nameof(RenderCallNode)} doesn't implement call with {n.Params} parameters")
			};

		static string RenderExpressionCallNodes(ImmutableArray<BuilderNode> expressionNodes) =>
			string.Join(
				"",
				expressionNodes
					.Where(n => n.Key == "Call")
					.Select(RenderExpressionNode)
			);

		static string RenderAnyNodes(ImmutableArray<BuilderNode> anyNodes) =>
			string.Join(
				"",
				anyNodes.Select(RenderAnyNode)
			);

		static string RenderAnyNode(BuilderNode n) =>
			n.Params switch
			{
				CallParams.Info | CallParams.Builder =>
			$$"""
					if ({{n.Builder}}.{{n.Check}}(info, builder))
						return Builder<{{n.Builder}}>.Instance;


			""",
				CallParams.Info =>
			$$"""
					if ({{n.Builder}}.{{n.Check}}(info))
						return Builder<{{n.Builder}}>.Instance;


			""",
				_ => throw new InvalidOperationException($"{nameof(RenderAnyNode)} doesn't implement call with {n.Params} parameters")
			};
	}
}
